import 'package:string_scanner/string_scanner.dart';
import 'ast.dart';

final RegExp _eof = RegExp(r'<<EOF>>', caseSensitive: false);
final RegExp _id = RegExp(r'[A-Za-z0-9_][A-Za-z0-9_]*');
final RegExp _reference = RegExp(r'{([A-Za-z0-9_][A-Za-z0-9_]*)}');
final RegExp _typeAttr = RegExp(r'<([A-Za-z0-9_][A-Za-z0-9_]*)>');
final RegExp _string = RegExp(r'"((([^\n"])|("))*)"');
final RegExp _whitespace = RegExp(r'[ \r\n\t]+');
final RegExp _unicode = RegExp(r'\\u([A-Fa-f0-9]+)');

String _error(SpanScanner scanner, String msg) {
  var span = scanner.emptySpan;
  return '${span.start.toolString}: $msg\n${span.highlight()}';
}

String _applyEscape(String s) {
  return s
      .replaceAll('\\"', '"')
      .replaceAll('\\b', '\b')
      .replaceAll('\\f', '\f')
      .replaceAll('\\r', '\r')
      .replaceAll('\\n', '\n')
      .replaceAllMapped(_unicode, (m) {
    String.fromCharCode(int.parse(m[1], radix: 16));
  });
}

Grammar parseLexGrammar(SpanScanner scanner) => _parseGrammar(scanner, true);

Grammar parseYaccGrammar(SpanScanner scanner) => _parseGrammar(scanner, false);

Grammar _parseGrammar(SpanScanner scanner, bool asLexerGrammar) {
  // Parse options
  var optionsSection = OptionsSection();
  var rulesSection = RulesSection();
  var freeTextSection = FreeTextSection();
  scanner.scan(_whitespace);

  while (!scanner.isDone && (!scanner.scan('%%') || scanner.scan('\\%%'))) {
    var dartCode = parseDartCode(scanner, withPercent: true);
    if (dartCode == null) {
      var alias = asLexerGrammar ? parseAlias(scanner) : null;

      if (alias == null) {
        var option = parseOption(scanner);
        if (option == null) {
          break;
        } else {
          optionsSection.options.add(option);
        }
      } else {
        optionsSection.aliases.add(alias);
      }
    } else {
      optionsSection.dartCode.add(dartCode);
      scanner.scan(_whitespace);
    }
  }

  scanner.scan(_whitespace);

  var rule = parseRule(scanner, asLexerGrammar);

  if (rule != null) {
    while (rule != null) {
      rulesSection.rules.add(rule);
      scanner.scan(_whitespace);

      if (scanner.isDone || (scanner.matches('%%'))) {
        break;
      } else {
        rule = parseRule(scanner, asLexerGrammar);
      }
    }

    scanner.scan(';');
    scanner.scan(_whitespace);
    if (!scanner.isDone && scanner.scan('%%')) {
      freeTextSection.text = scanner.rest;
    }
  }

  return Grammar()
    ..optionsSection = optionsSection
    ..rulesSection = rulesSection
    ..freeTextSection = freeTextSection;
}

Option parseOption(SpanScanner scanner) {
  if (!scanner.scan('%') || !scanner.scan(_id)) return null;
  var name = scanner.lastMatch[0], typeAttr;

  scanner.scan(_whitespace);
  if (scanner.scan(_typeAttr)) typeAttr = scanner.lastMatch[1];

  scanner.scan(_whitespace);
  var values = <OptionValue>[];
  var value = parseOptionValue(scanner);

  while (value != null) {
    values.add(value);
    scanner.scan(_whitespace);
    value = parseOptionValue(scanner);
  }

  if (values.isEmpty) throw _error(scanner, "Expected a value after '%$name'.");

  return Option()
    ..name = name
    ..typeAttr = typeAttr as String
    ..values.addAll(values);
}

OptionValue parseOptionValue(SpanScanner scanner) {
  var code = parseDartCode(scanner);
  if (code != null) return OptionValue()..dartCode = code;
  if (scanner.scan(_id)) return OptionValue()..reference = scanner.lastMatch[0];
  if (scanner.scan(_string)) {
    return OptionValue()..string = _applyEscape(scanner.lastMatch[1]);
  }
  return null;
}

Rule parseRule(SpanScanner scanner, bool asLexerRule) {
  Rule rule;

  if (asLexerRule && scanner.scan(_string)) {
    rule = Rule()..string = _applyEscape(scanner.lastMatch[1]);
  } else if (asLexerRule && scanner.scan(_reference)) {
    rule = Rule()..reference = scanner.lastMatch[1];
  } else if (asLexerRule && scanner.scan(_eof)) {
    rule = Rule()..eof = scanner.lastMatch[0];
  } else if (!asLexerRule && scanner.scan(_id)) {
    rule = Rule()..reference = scanner.lastMatch[0];
  } else if (asLexerRule) {
    var b = StringBuffer();

    while (!scanner.isDone) {
      if (scanner.matches('\\;') || scanner.matches('\\{')) continue;
      if (scanner.matches(';') ||
          scanner.matches('{') ||
          scanner.matches('\n')) {
        break;
      }
      b.writeCharCode(scanner.readChar());
    }

    var str = b.toString().trim();
    if (str.isNotEmpty) rule = Rule()..regExp = RegExp(str);
  }

  if (rule == null) return null;

  if (asLexerRule) {
    scanner.scan(_whitespace);
    if ((rule.dartCode = parseDartCode(scanner)) == null) {
      if (!scanner.scan(';')) throw _error(scanner, "Expected ';' or '{'.");
    }
  } else {
    scanner.scan(_whitespace);

    if (!scanner.scan(':')) {
      throw _error(scanner, "Missing ':' after '${rule.reference}.'");
    }
    scanner.scan(_whitespace);

    var alt = parseAlternative(scanner);

    while (alt != null) {
      rule.alternatives.add(alt);
      scanner.scan(_whitespace);
      if (!scanner.scan('|') || scanner.scan(';')) break;
      scanner.scan(_whitespace);
      alt = parseAlternative(scanner);
    }

    if (rule.alternatives.isEmpty) {
      throw _error(scanner, "Rule '${rule.reference}' has no alternatives.");
    }
  }

  return rule;
}

Alias parseAlias(SpanScanner scanner) {
  if (!scanner.scan(_id)) return null;
  var name = scanner.lastMatch[0];
  scanner.scan(_whitespace);
  if (!scanner.scan('=')) throw _error(scanner, "Missing '=' after '$name'.");
  var b = StringBuffer();

  while (!scanner.isDone && !scanner.scan('\n')) {
    b.writeCharCode(scanner.readChar());
  }

  var str = b.toString().trim();
  if (str.isEmpty) {
    throw _error(scanner, "Missing regular expression for alias '$name'.");
  }
  return Alias()
    ..name = name
    ..regExp = RegExp(str);
}

Alternative parseAlternative(SpanScanner scanner) {
  var references = <RuleReference>[];
  String last;
  scanner.scan(_whitespace);

  while (scanner.matches(_id) || scanner.matches(_string)) {
    var ref = RuleReference();
    if (scanner.scan(_id)) {
      ref.reference = last = scanner.lastMatch[0];
    } else if (scanner.scan(_string)) {
      ref.string = last = _applyEscape(scanner.lastMatch[1]);
    }
    references.add(ref);
    scanner.scan(_whitespace);
  }

  if (references.isEmpty) return null;

  var dartCode = parseDartCode(scanner);
  if (dartCode == null) {
    throw _error(scanner, "Missing predicate after alternative '$last'.");
  }

  return Alternative()
    ..references.addAll(references)
    ..dartCode = dartCode;
}

DartCode parseDartCode(SpanScanner scanner, {bool withPercent = false}) {
  if (!scanner.scan(withPercent ? '%{' : '{')) return null;
  var b = StringBuffer();
  while (!scanner.isDone &&
      (!scanner.scan(withPercent ? '%}' : '}') ||
          scanner.scan(withPercent ? '\\%}' : '\\}'))) {
    b.writeCharCode(scanner.readChar());
  }
  return DartCode()..text = b.toString();
}
